/**
 * Copyright (c) 2022 Beijing Jiaotong University
 * PhotLab is licensed under [Open Source License].
 * You can use this software according to the terms and conditions of the [Open Source License].
 * You may obtain a copy of [Open Source License] at: [https://open.source.license/]
 *
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 *
 * See the [Open Source License] for more details.
 *
 * Author: Zhenjie Wei
 * Created: Mar. 13, 2023
 * Supported by: National Key Research and Development Program of China
 */

import { Disposable } from '@/common/disposable'
import {
    graphService as graphAPI,
    libraryService as libraryAPI,
} from './client'
import { RenderGraph } from '@/common/types/ws_types'
import {
    RenderNode,
    RenderPair,
    RenderPath,
    RenderPort,
    WayPoint,
} from '@/common/types/ws_types'
import { Rect, Point, Size } from '@/common/types/types2D'
import { uid_rt, uid_add, uid_del, uid } from '@/common/types/base_types'
import { commandManager } from '@/common/cmd/manager'
import {
    PhotGraph,
    RenderPort as PhotPort,
    RenderRoute as PhotRoute,
} from '@/common/types/phot_types'
import LogicCore, { ILogicPlugin } from '@/common/logic/core'
import Component from '@/components/phot_system/work_space/objects/comp'

export const graphManager = new (class GraphManager
    extends Disposable
    implements ILogicPlugin
{
    private _logicCore: LogicCore | null = null
    private _currentGraphName: string | null = null
    private _currentGraphId: uid = 0
    private _currentPhotGraph: PhotGraph | null = null
    private _currentRenderGraph: RenderGraph = new RenderGraph(0)

    public install(core: LogicCore) {
        this._logicCore = core
        core.malloc('render-graph', this, {
            _currentGraphId: 1,
            _currentGraphName: 1,
            _currentPhotGraph: 2,
            _currentRenderGraph: 2,
        })
        core.on('comp.move', (compId: uid, pos: Point) => {
            const affectedPaths = this._currentRenderGraph?.moveNodeTo(
                compId,
                pos
            )
            this._moveNodeTo(compId, pos, affectedPaths)
        })
        core.on('select.logic-changed', () => {
            // this._currentRenderGraph?.getNode(compId)
            console.log(core.recentSelectedLogicObject)
        })
    }

    public uninstall() {
        this._logicCore = null
    }

    constructor() {
        super()
        return
    }

    public get graphName(): string | null {
        return this._currentGraphName
    }

    public get graphId(): uid | null {
        return this._currentGraphId
    }

    public get photGraph(): any | null {
        return this._currentPhotGraph
    }

    public get renderGraph(): RenderGraph | null {
        return this._currentRenderGraph
    }

    public get logicCore(): LogicCore | null {
        return this._logicCore
    }

    public switchGraph(graphId: uid) {
        console.group('graphManager.switchGraph')
        const currentId = this._currentGraphId
        const target = this._logicCore?.switchMemory(graphId)
        if (!target || target === currentId) {
            console.warn(
                'graphManager.switchGraph failed. switch memory failed.'
            )
        } else {
            console.log('switched to graph: ' + graphId)
        }
        console.groupEnd()
    }

    public async openGraph(path: string) {
        console.group('graphManager.openGraph')
        // console.log(path)
        const uid = uid_rt()
        const photGraph = await graphAPI.openGraph(path)
        // const graphId = parseInt(photGraph.graphId)
        // console.log("opened graph: ", path)
        console.log('_currentPhotGraph: ', this._currentPhotGraph)
        console.log('_currentGraphId: ', this._currentGraphId)
        console.log('_currentGraphName:', this._currentGraphName)
        if (!photGraph.name) {
            return Promise.reject(
                'graphManager.openGraph failed. name not found.'
            )
        }
        const rGraph = await graphAPI.getRenderGraph(photGraph.graphId)
        console.log('renderGraphObj: ', rGraph)
        const renderGraph = new RenderGraph(uid)
        this._logicCore?.createMemory(uid)
        this._logicCore?.switchMemory(uid)
        this._currentPhotGraph = photGraph
        this._currentGraphName = photGraph.name
        this._currentGraphId = uid
        this._currentRenderGraph = renderGraph
        // const rGraph = await graphAPI.getRenderGraph(this._currentGraphId.toString())
        // console.log("renderGraphObj: ", rGraph)
        // construct render graph from received data
        // add nodes
        for (const node of rGraph.nodes) {
            // console.log(node)
            uid_add(node.nodeId)
            console.log(node)
            const rNode = new RenderNode(
                node.nodeId,
                Rect.fromLTWH(
                    node.rect.x!,
                    node.rect.y!,
                    node.rect.width!,
                    node.rect.height!
                ),
                node.ports.map((port: any) => RenderPort.fromObject(port)),
                node.componentName,
                node.iconName,
                node.description
            )
            renderGraph.addNode(rNode)
            const component = new Component(rNode)
            const success = this._logicCore?.register(component)
            if (success) {
                console.log('addComponentFromLibrary.registered')
                const compLayer = this._logicCore?.getLayer('comp')
                if (compLayer) {
                    ;(compLayer as any).addComponent(component)
                }
            } else {
                console.warn('addComponentFromLibrary.register failed')
                this._currentRenderGraph?.delNode(rNode.id)
            }
        }
        console.log('node renderGraph: ', renderGraph)
        // add links
        // rGraph.routes.forEach((route: PhotRoute, index: number, array: any[]) => {
        //     const id = route.routeId
        //     array[index].routeId = id
        // })
        // console.log("rGraph.routes", rGraph.routes)
        // console.log('photGraph', photGraph)
        let linkIdx = 0
        for (const link of photGraph.links) {
            if (linkIdx >= rGraph.routes.length) {
                throw new Error(
                    'graphManager.openGraph failed. route not found.'
                )
            }
            const srcNode = renderGraph.getNode(link.srcNodeId)
            if (!srcNode) {
                return Promise.reject(
                    'graphManager.openGraph failed. link src node not found.'
                )
            }
            const dstNode = renderGraph.getNode(link.dstNodeId)
            if (!dstNode) {
                return Promise.reject(
                    'graphManager.openGraph failed. link dst node not found.'
                )
            }
            const srcPort = srcNode.getPort(link.srcPortId)
            const dstPort = dstNode.getPort(link.dstPortId)
            const srcPair = new RenderPair(srcNode, srcPort)
            const dstPair = new RenderPair(dstNode, dstPort)
            // console.log("srcPair: ", srcPair)
            // console.log("dstPair: ", dstPair)
            const success = renderGraph.addLink(
                srcPair,
                dstPair,
                RenderPath.fromPoints(
                    rGraph.routes[linkIdx++].path.map((p: any) => [
                        new Point(p.x, p.y),
                        p.direction,
                    ])
                ),
                link.linkId
            )
            if (!success) {
                return Promise.reject(
                    'graphManager.openGraph failed. add link failed.'
                )
            }
        }
        console.log('link renderGraph: ', renderGraph)
        // console.log("renderGraph: ", renderGraph)
        this._logicCore?.renderAll()
        console.groupEnd()
    }

    public async getGraph(graphId: string) {
        console.log('graphManager.getGraph')
        if (!graphId) {
            return Promise.reject(
                'graphManager.getGraph failed. graph id not found.'
            )
        }
        return (await graphAPI.getGraph(graphId)) || null
        // return await graphAPI.getGraph(graphId.toString()) || null
    }
    public async getGraphParameter(graphParameterName: string) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.log('graphManager.getGraphParameter')
        if (!graphParameterName) {
            return Promise.reject(
                'graphManager.getGraphParameter failed. graph parameter name not found.'
            )
        }
        return await graphAPI.getGraphParameter(
            this.photGraph.graphId,
            graphParameterName
        )
        // return await graphAPI.getGraphParameter(this._currentGraphId.toString(), graphParameterName)
    }
    public async getGraphParameters() {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.log('graphManager.getGraphParameters')
        return await graphAPI.getGraphParameters(this.photGraph.graphId)
        // return await graphAPI.getGraphParameters(this._currentGraphId.toString())
    }
    public async alterGraphParameters(parameters: any) {
        console.log(parameters)
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        try {
            await graphAPI.alterGraphParameters(
                this.photGraph.graphId,
                parameters
            )
        } catch (err: any) {
            console.error(err)
            return Promise.reject(err)
        }
        // return await graphAPI.getGraphParameters(this._currentGraphId.toString())
    }
    public async addGraphParameter(parameter: any) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        try {
            await graphAPI.addGraphParameter(this.photGraph.graphId, parameter)
        } catch (err: any) {
            console.error(err)
            return Promise.reject(err)
        }
        // return await graphAPI.getGraphParameters(this._currentGraphId.toString())
    }
    public async getOpenedGraphList() {
        return (await graphAPI.getOpenedGraphList()) || []
    }
    public async closeGraph(uid: uid, graphId: string) {
        console.log('graphManager.closeGraph')
        if (!uid) {
            return Promise.reject(
                'graphManager.closeGraph failed. graph id not found.'
            )
        }
        await graphAPI.closeGraph(graphId, true)

        this._logicCore?.deleteMemory(uid)
    }
    public async closeAllOpenedGraph() {
        await graphAPI.closeAllOpenedGraph()
    }
    public async newGraph(name: string) {
        console.group('graphManager.newGraph')
        const photGraph = await graphAPI.newGraph(name)
        console.log('new graph: ', name)
        const graphId = uid_rt()
        const renderGraph = new RenderGraph(graphId)
        console.log('renderGraph: ', renderGraph)
        this._logicCore?.createMemory(graphId)
        this._logicCore?.switchMemory(graphId)
        this._currentGraphName = name
        this._currentGraphId = graphId
        this._currentPhotGraph = photGraph
        this._currentRenderGraph = renderGraph
        this._logicCore?.renderAll()
        console.groupEnd()
    }

    private async _moveNodeTo(
        compId: number,
        pos: Point,
        paths: Map<uid, RenderPath>
    ) {
        console.log('moveNodeTo', pos)
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        await graphAPI.RenderNodeMoveTo(
            this._currentPhotGraph!.graphId,
            compId,
            pos.x,
            pos.y
        )
        for (const [linkId, path] of paths) {
            await graphAPI.setRenderRoutePath(
                this._currentPhotGraph!.graphId,
                linkId,
                path.object
            )
        }
    }

    public async saveGraph(graphId?: string) {
        console.log('graphManager.saveGraph')
        if (!graphId) {
            if (!this._currentGraphId) {
                return Promise.reject('currentGraphId is null.')
            }
            graphId = this.photGraph.graphId
        }
        if (!graphId) {
            return Promise.reject(
                'graphManager.saveGraph failed. graph id not found.'
            )
        }
        await graphAPI.saveGraph(graphId)
    }

    public async saveGraphAs(path: string) {
        console.group('graphManager.saveGraphAs ', path)
        if (!this._currentGraphId) {
            return Promise.reject(
                'graphManager.saveGraphAs failed. graph id not found.'
            )
        }
        await graphAPI.saveGraphAs(path, this._currentPhotGraph!.graphId)
        console.groupEnd()
    }

    public async getComponent(componentId: number, graphId?: any) {
        if (!graphId) {
            graphId = this.photGraph.graphId
            if (!this._currentGraphId) {
                return Promise.reject('currentGraphId is null.')
            }
        }
        console.group('graphManager.getComponent')
        if (!componentId) {
            return Promise.reject(
                'graphManager.getComponent failed. component id not found.'
            )
        }
        console.log('graphId:', this._currentGraphId)
        console.log('componentId:', componentId)
        const component: any = await graphAPI.getComponent(graphId, componentId)
        // const component: any = await graphAPI.getComponent(this._currentGraphId.toString(), componentId)
        console.groupEnd()
        return component
    }
    public async getRenderNode(componentId: number, graphId?: any) {
        if (!graphId) {
            graphId = this.photGraph.graphId
            if (!this._currentGraphId) {
                return Promise.reject('currentGraphId is null.')
            }
        }
        console.group('graphManager.getComponent')
        if (!componentId) {
            return Promise.reject(
                'graphManager.getComponent failed. component id not found.'
            )
        }
        console.log('graphId:', this._currentGraphId)
        console.log('componentId:', componentId)
        const renderNode: any = await graphAPI.getRenderNode(
            graphId,
            componentId
        )
        // const component: any = await graphAPI.getComponent(this._currentGraphId.toString(), componentId)
        console.groupEnd()
        return renderNode
    }

    public async getComponents() {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.getComponents')
        const components: any =
            (await graphAPI.getComponents(this.photGraph.graphId)) || []
        // const components: any = await graphAPI.getComponents(this._currentGraphId.toString()) || []
        console.groupEnd()
        return components
    }
    public async alterComponent(component: any) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.alterComponent')
        console.log(component)
        await graphAPI.alterComponent(this.photGraph.graphId, component)
        // await graphAPI.alterComponent(this._currentGraphId.toString(), component)
        console.groupEnd()
    }
    // 面向前端的统一接口，管理后端phot/render和前端的renderGraph
    public async deleteComponent(componentId: any) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.deleteComponent')
        const comp = this._currentRenderGraph?.getNode(componentId)
        if (!comp) {
            console.warn('graphManager.deleteComponent failed. node not found.')
            return
        }
        
        await graphAPI.deleteComponent(
            this.photGraph.graphId,
            componentId,
            false
        )
        await graphAPI.deleteRenderNode(
            this.photGraph.graphId,
            componentId,
            false
        )
        this._currentRenderGraph?.delNode(componentId)
        console.groupEnd()
    }

    public async deleteRenderRoute(
        componentId: any,
        cascadeDelete: boolean = false
    ) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        await graphAPI.deleteRenderRoute(
            this.photGraph.graphId,
            componentId,
            cascadeDelete
        )
        // await graphAPI.deleteRenderRoute(this._currentGraphId.toString(), componentId, cascadeDelete)
    }
    public async deleteTextArea(textId: number) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        await graphAPI.deleteTextArea(this.photGraph.graphId, textId)
        // await graphAPI.deleteTextArea(this._currentGraphId.toString(), textId)
    }
    public async addComponentFromLibrary(path: string, crd: Point) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.addComponentFromLibrary')
        if (!this._currentGraphName) {
            return Promise.reject(
                'graphManager.addComponentFromLibrary failed. graph name not found.'
            )
        }
        const prototype = await libraryAPI.getComponentPrototype(path)
        console.log('prototype: ', prototype)
        const size = new Size(prototype.rect!.width, prototype.rect!.height)
        const photRect = {
            x: crd.x,
            y: crd.y,
            width: size.width,
            height: size.height,
        }
        const comp = await graphAPI.addComponentFromLibrary(
            this._currentPhotGraph!.graphId,
            path,
            photRect
        )
        const ports = prototype.ports.map((port: any) =>
            RenderPort.fromObject(port)
        )
        console.log('addComponentFromLibrary.ports: ', prototype.ports, ports)
        const node = new RenderNode(
            comp.componentId,
            Rect.fromLTWH(crd.x, crd.y, size.width, size.height),
            ports,
            prototype.name,
            prototype.iconName
        )
        this._currentRenderGraph?.addNode(node)
        const component = new Component(node)
        const success = this._logicCore?.register(component)
        if (success) {
            console.log('addComponentFromLibrary.registered')
            const compLayer = this._logicCore?.getLayer('comp')
            if (compLayer) {
                ;(compLayer as any).addComponent(component)
            }
        } else {
            console.warn('addComponentFromLibrary.register failed')
            this.deleteComponent(comp.componentId)
        }
        console.groupEnd()
        return success
    }

    public async addLink(from: RenderPair, to: RenderPair, path: RenderPath) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.addLink')
        const linkId =
            this._currentRenderGraph?.addLink(from, to, path) ?? false
        if (!linkId) {
            return Promise.reject('graphManager.addLink failed')
        }
        const rLink = this._currentRenderGraph?.getLink(linkId)!
        console.log('rLink: ', rLink)
        try {
            const obj = rLink.object
            console.log('obj: ', obj)
            await graphAPI.addLink(this.photGraph.graphId, obj)
            // await graphAPI.addLink(this._currentGraphId.toString(), rLink.object)
            console.log('path: ', path)
            await graphAPI.addRenderRoute(this.photGraph.graphId, {
                // await graphAPI.addRenderRoute(this._currentGraphId.toString(), {
                routeId: linkId,
                path: path.object,
            })
        } catch (err) {
            // rollback
            this._currentRenderGraph?.delLink(from, to)
            return Promise.reject(err)
        }
        console.groupEnd()
    }

    public async delLink(from: RenderPair, to: RenderPair) {
        if (!this._currentGraphId) {
            return Promise.reject('currentGraphId is null.')
        }
        console.group('graphManager.delLink')
        const link = this._currentRenderGraph?.delLink(from, to)
        console.log('link: ', link)
        if (!link) {
            return Promise.reject('graphManager.delLink failed')
        }
        try {
            await graphAPI.deleteLink(this.photGraph.graphId, link.id, false)
            await graphAPI.deleteRenderRoute(
                this.photGraph.graphId,
                link.id,
                false
            )
            // await graphAPI.deleteLink(this._currentGraphId.toString(), link.id, false)
            // await graphAPI.deleteRenderRoute(this._currentGraphId.toString(), link.id, false)
        } catch (err) {
            // rollback
            this._currentRenderGraph?.addLink(from, to)
            return Promise.reject(err)
        }
        console.groupEnd()
    }

    public activate() {
        // console.log("graphManager.activated")
        // graphAPI.
        // graphAPI.closeGraph("testGraph", true)
        commandManager.registerCommand(
            'test.workspace.graph.clear',
            async () => {
                console.log('test.workspace.graph.clear')
            }
        )
        commandManager.registerCommand(
            'test.workspace.graph.close',
            async () => {
                await graphManager.closeGraph(
                    this._currentGraphId,
                    this.photGraph.graphId
                )
            }
        )
        commandManager.registerCommand(
            'test.workspace.graph.save',
            async () => {
                await graphManager.saveGraph()
            }
        )
    }
})()
